//
// Copyright 2010 Marcel Urbanek
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//      http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package de.urbiworx.typewriter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseStream;
import javax.servlet.http.HttpServletRequest;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.batik.css.engine.sac.CSSConditionFactory;
import org.apache.batik.css.engine.sac.CSSSelectorFactory;
import org.apache.batik.css.parser.Parser;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;
import org.w3c.css.sac.InputSource;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class FoJsfOutputStream extends ResponseStream {
	public static final String TYPEWRITER_URL = "TYPEWRITER_URL";
	public static final String TYPEWRITER_RESULT = "TYPEWRITER_RESULT";
	private static final String XSL_PREFIX = "fo";
	private static final String XSL_NS = "http://www.w3.org/1999/XSL/Format";
	
	Logger logger=Logger.getLogger("de.urbiworx.typewriter");
	  private ByteArrayOutputStream output = null;
	  private boolean closed = false;
	  
	public byte[] getBytes(){
		  return output.toByteArray();
	  }

	  
	  public FoJsfOutputStream() throws IOException {
		    super();
		    closed = false;
		    output = new ByteArrayOutputStream();
		  }
	  
	  

	  @Override
	  public void close() throws IOException {
	    if (closed) {
	      throw new IOException("stream has already been closed");
	    }
	    closed = true;
    	PdfResult result=buildPdf(this);
    	FacesContext context=FacesContext.getCurrentInstance();
    	HttpServletRequest request=(HttpServletRequest)context.getExternalContext().getRequest();
    	context.getExternalContext().getSessionMap().put(TYPEWRITER_RESULT, result);
    	context.getExternalContext().getSessionMap().put(TYPEWRITER_URL, request.getRequestURL());
    	context.getExternalContext().redirect(request.getRequestURL().toString());
    	context.responseComplete();
	  }
	  
	  @Override
	  public void flush() throws IOException {
	    if (closed) {
	      throw new IOException("stream has already been closed");
	    }
	    output.flush();
	  }

	  @Override
	  public void write(int b) throws IOException {
	    if (closed) {
	      throw new IOException("Cannot write to a closed output stream");
	    }
	    // write the byte to the temporary output
	    output.write((byte) b);
	  }

	  @Override
	  public void write(byte[] b) throws IOException {
	    write(b, 0, b.length);
	  }
	  
	  @Override
	  public void write(byte[] b, int off, int len) throws IOException {
	    if (closed) {
	      throw new IOException("Cannot write to a closed output stream");
	    }
	    output.write(b, off, len);
	  }

	  public void reset() {

	  }
	  
	  
		private PdfResult buildPdf(FoJsfOutputStream foOutput){
			byte[] responseText=foOutput.getBytes();
			if (responseText==null){
				return null;
			}
			ByteArrayOutputStream baos=new ByteArrayOutputStream();
			try{
				
				//Creating XML Tree from XSL FO
				DocumentBuilderFactory builderFactory=DocumentBuilderFactory.newInstance();
				builderFactory.setNamespaceAware(true);
				Document doc=builderFactory.newDocumentBuilder().parse(new ByteArrayInputStream(responseText));

				// Finding all "style" elements in fo document and creating css selectors
	        	Parser p = new Parser();
	           
	            p.setSelectorFactory(CSSSelectorFactory.INSTANCE);
	            p.setConditionFactory(new CSSConditionFactory(null,null,null,null));
	            
	            List<ComplexSelector> selectors =new ArrayList<ComplexSelector>();

				XPathFactory factory = XPathFactory.newInstance();
				XPath xpath = factory.newXPath();
				
				xpath.setNamespaceContext(new NamespaceContext(){
					public String getNamespaceURI(String prefix) {
						if (XSL_PREFIX.equals(prefix)){
							return XSL_NS;
						}
						return null;
					}
					public String getPrefix(String namespaceURI) {
						if (XSL_NS.equals(namespaceURI))
						{
							return XSL_PREFIX;
						}
						return null;
					}
					public Iterator getPrefixes(String namespaceURI) {
						if (XSL_NS.equals(namespaceURI))
						{
							List retList=new LinkedList();
							retList.add(XSL_PREFIX);
							return retList.iterator();
						}
						return (new LinkedList()).iterator();
					}
				});
				
				XPathExpression expr = xpath.compile("//fo:style");

				NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
				Node currentStyleNode;
				for (int i=0;i<result.getLength();i++){
					logger.info("Found style element block");
					currentStyleNode=result.item(i);
					Node text=(Node) currentStyleNode.getChildNodes().item(0);	
					ExtractDocumentHandler extractor = new ExtractDocumentHandler();
					p.setDocumentHandler(extractor);
		            p.parseStyleSheet(new InputSource(
		            		new StringReader(text.getTextContent())
		            		)
		            );
					selectors.addAll(extractor.getAllSelectors());
					currentStyleNode.getParentNode().removeChild(currentStyleNode);
				}  
	            
				//Sorting css selectors according to their weight
	            Collections.sort(selectors);
	            
	            CSSApplyer tempApplyer=new CSSApplyer(doc,selectors);
	            tempApplyer.merge();
	            
	            //Transforming XML to PDF
				FopFactory fopFactory = FopFactory.newInstance();
				TransformerFactory tFactory = TransformerFactory.newInstance();    
		        Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, baos);
		        
		        Transformer transformer = tFactory.newTransformer();
		        Source src = new DOMSource(tempApplyer.getMergedDocument());
		        Result res = new SAXResult(fop.getDefaultHandler());
		        transformer.transform(src, res);
			}
			catch (Exception e){
				logger.log(Level.SEVERE,"Error while trying to create PDF",e);
				StringBuilder builder=new StringBuilder();
				builder.append(e.getMessage());
				builder.append("<pre>");
				String formattedFo=new String(responseText);
				formattedFo=formattedFo.replaceAll("<", "&lt;");
				formattedFo=formattedFo.replaceAll(">", "&gt;");
				
				builder.append(formattedFo.getBytes());
				builder.append("</pre>");
				return new PdfResult("text/html",builder.toString().getBytes());
			}

			return new PdfResult(MimeConstants.MIME_PDF,baos.toByteArray());
		}
		protected class PdfResult{
			protected PdfResult(String contentType,byte[] content){
				this.contentType=contentType;
				this.content=content;
			}
			protected String contentType;
			protected byte[] content;
		}
	}